#include "libAcoustX/solver/bhp/BHPCaller.h"
#include "libAcoustX/solver/bhp/BHPShdParser.h"
#include "libAcoustX/solver/bhp/BHPSedimentDef.h"
#include "libGeoutils/tool/shape_tool.h"
#include "libGeoutils/tool/datetime_tool.h"
#include "Poco/Path.h"
#include "Poco/File.h"
#include "Poco/FileStream.h"
#include "Poco/Process.h"
#include "Poco/Pipe.h"
#include "Poco/StreamCopier.h"
#include "Poco/Timestamp.h"
#include "Poco/DateTimeFormatter.h"
#include "Poco/Exception.h"
#include <sstream>
#include <iomanip>
#include <memory>

#define FILE_SPLIT_CHAR " "
#define FILE_END_CHAR "/"
#define BELLHOP_ENVFILE_PREFIX "Axbhp"
#define BELLHOP_PATH_NAME "Axio"
#define BELLHOP_EXECUTABLE "bellhop"
#define BELLHOP_EXECUTABLE_SUCCESS_KEY "target_is_solved"

namespace ax {
namespace bhp {
class BhpCallerPrivate 
{
public:
	/*
	* input
	*/
	ax::ocean::Dataset::sptr mOceanData;
	ax::core::Source::sptr mSource;
	ax::param::Line::sptr mParam;
	std::string mBhpExePath;

public:
	/*
	* mid params
	*/
	unsigned mID;
	std::string mIdString;
	double mFreq = 1000;
	double mSourceLon; 
	double mSourceLat; 
	double mSourceDepth = 50;
	double mReceiverLon; 
	double mReceiverLat;
	double mAngleStart = -90;
	double mAngleEnd = 90;
	std::string mRayOption_1 = "I";
	std::string mRayOption_2 = "B";
	unsigned mDepthNumber = 500;
	unsigned mRangeNumber = 1000;
	unsigned mRayNumber = 5000;
	unsigned mRangeCoordsNumber = 51;

public:
	/*
	* calc
	*/
	Poco::Path mCreatorPath;
	std::stringstream mEnvSS, mBtySS, mSspSS, mLogSS;

	short mSourceBathy, mBoxMaxDepth, mProfileMaxBathy;
	std::vector<std::pair<double, double>> mSourceSSPData;
	double mDistance, mBoxMinRange, mBoxMaxRange;

	std::vector<double> mSSPDisVector;
	std::vector<std::pair<double, double>> mSSPCoord;
	std::vector<std::pair<double, std::vector<double>>> mSSPVector;
	unsigned mSSPHeight;

	std::vector<double> mBathyDisVector;
	std::vector<std::pair<double, double>> mBathyCoord;
	std::map<double, short> mBathyVector;

public:
	/*
	* output
	*/
	std::string mLastErrorMessage;

	bool parse_param() 
	{
		if (!mParam) {
			mLastErrorMessage = "AxError: param nullptr!";
			return false;
		}

		if (!mSource) {
			mLastErrorMessage = "AxError: source nullptr!";
			return false;
		}

		mID = mParam->getOptSolveNumber();
		mIdString = mParam->getOptSolveID();
		mFreq = mSource->getFrequency();
		mSourceLon = mSource->getLongitude();
		mSourceLat = mSource->getLatitude();
		mSourceDepth = mSource->getAltitude();
		mReceiverLon = mParam->getTargetLongitude();
		mReceiverLat = mParam->getTargetLatitude();
		mAngleStart = mParam->getAngleStartDe();
		mAngleEnd = mParam->getAngleEndDe();
		mDepthNumber = mParam->getTargetDepthNumber();
		mRangeNumber = mParam->getTargetRangeNumber();
		mRayNumber = mParam->getOptRayNumber();
		mRangeCoordsNumber = mParam->getTargetRangeCoordNumber();

		if (mIdString.empty()) {
			mIdString = geo::tool::DateTime::now()->toString();
		}

		return true;
	}
	bool init_member()
	{
		if (!mOceanData) {
			mLastErrorMessage = "AxError: ocean nullptr!";
			return false;
		}
		{
			geo::tool::ShapeTool st;
			st.calc_coord_distance(geo::shape::Coord(mSourceLon, mSourceLat), geo::shape::Coord(mReceiverLon, mReceiverLat), mDistance);
			mBoxMinRange = -(mDistance + mDistance * 0.1);
			mBoxMaxRange = -mBoxMinRange;
		}
		{
			mSSPDisVector.emplace_back(mBoxMinRange);
			double range_step = mDistance / (mRangeCoordsNumber - 1);
			for (int i = 0; i < mRangeCoordsNumber; ++i) {
				mSSPDisVector.emplace_back(i * range_step);
				mBathyDisVector.emplace_back(i * range_step);
			} 
			mSSPDisVector.emplace_back(mBoxMaxRange);
		}
		{
			mSourceBathy = mOceanData->getDepth(mSourceLon, mSourceLat, 100);
			if (mSourceBathy > 0) {
				mLastErrorMessage = "AxError: source coord is on land";
				return false;
			}
			mSourceBathy = -mSourceBathy;
		}
		{
			double lon_diff = mReceiverLon - mSourceLon;
			double lat_diff = mReceiverLat - mSourceLat;
			double oppo_lon = mSourceLon - lon_diff;
			double oppo_lat = mSourceLat - lat_diff;
			double box_max_lon = mReceiverLon + 0.1 * lon_diff;
			double box_max_lat = mReceiverLat + 0.1 * lat_diff;

			mSSPCoord.push_back({ oppo_lon, oppo_lat });
			double lon_step = lon_diff / (mRangeCoordsNumber - 1);
			double lat_step = lat_diff / (mRangeCoordsNumber - 1);

			for (int i = 0; i < mRangeCoordsNumber; ++i) {
				double loop_lon = mSourceLon + i * lon_step;
				double loop_lat = mSourceLat + i * lat_step;
				mSSPCoord.push_back({ loop_lon, loop_lat });
				mBathyCoord.push_back({ loop_lon, loop_lat });
			} 
			mSSPCoord.push_back({ box_max_lon, box_max_lat });
		}
		{
			// mProfileMaxBathy = INT16_MIN;
			short loop_value;
			for (int i = 0; i < mBathyCoord.size(); ++i) {
				auto loop_bathy_coord = mBathyCoord[i];
				loop_value = mOceanData->getDepth(loop_bathy_coord.first, loop_bathy_coord.second);
				mBathyVector[mBathyDisVector[i]] = -loop_value;
				if (-loop_value > mSourceBathy) {
					mSourceBathy = -loop_value;
				}
			}
			mProfileMaxBathy = mSourceBathy;
			mBoxMaxDepth = mSourceBathy + mSourceBathy * 0.1;
		}
		{
			auto source_depth_ssp = mOceanData->getSsp(mSourceLon, mSourceLat);
			for (const auto& loop_depth_ssp : source_depth_ssp) {
				std::pair<double, double> loop_pair;
				loop_pair.first = loop_depth_ssp.first;
				loop_pair.second = loop_depth_ssp.second;
				mSourceSSPData.push_back(loop_pair);
			}
			for (auto it = mSourceSSPData.begin(); it != mSourceSSPData.end(); ++it) {
				if (it->first > mSourceBathy) {
					mSSPHeight = std::distance(mSourceSSPData.begin(), it);
					break;
				}
			}
			mSourceSSPData.resize(mSSPHeight);
			mSourceSSPData.push_back({ mSourceBathy, mSourceSSPData.back().second });
		}
		{
			for (int i = 1; i < mSSPCoord.size(); ++i) 
			{
				auto loop_ssp_coord = mSSPCoord[i];

				std::vector<double> loop_value;
				auto source_depth_ssp = mOceanData->getSsp(loop_ssp_coord.first, loop_ssp_coord.second);
				for (const auto& loop_depth_ssp : source_depth_ssp) {
					loop_value.push_back(loop_depth_ssp.second);
				}

				loop_value.resize(mSSPHeight);
				loop_value.push_back(loop_value.back());
				mSSPVector.push_back({ mSSPDisVector[i], std::move(loop_value) });
			} 
			
			mSSPVector.insert(mSSPVector.begin(), { mSSPDisVector[0], mSSPVector[0].second });
		}

		return true;
	}
	bool find_path()
	{
		try {
			Poco::Path base_dir(BELLHOP_PATH_NAME);
			Poco::Path id_str_path(base_dir, mIdString);
			Poco::Path freq_dir_name = Poco::Path(id_str_path, "freq" + std::to_string(static_cast<int>(mFreq)));
			mCreatorPath = Poco::Path(freq_dir_name, std::to_string(mID));
			Poco::File work_dir_file(mCreatorPath);
			if (!work_dir_file.exists()) {
				work_dir_file.createDirectories();
			}
			else if (!work_dir_file.isDirectory()) {
				mLastErrorMessage = "AxError: bhp input files path isn't a Dir";
				return false;
			} 
			return true;
		}
		catch (...) {
			mLastErrorMessage = "AxError: find_path func catch a Exception";
			return false;
		}
	}
	bool write_cfg()
	{
		Poco::Path cfg_file_path(mCreatorPath, BELLHOP_ENVFILE_PREFIX + std::string(".json"));
		mParam->toJsonFile(cfg_file_path.toString());
		return true;
	}
	bool write_env()
	{
		try
		{
			mEnvSS.clear();
			mEnvSS << "'" << "Made By ax_bhp_solver_gohinn(V1.0) - "
				<< "source(" << mSourceLon << "," << mSourceLat << ")" << " - "
				<< "receiver(" << mReceiverLon << "," << mReceiverLat << ")"
				<< "'" << std::endl;

			// !FREQUENCY [HZ]
			mEnvSS << mFreq << std::endl;
			// !NMEDIA
			mEnvSS << 1 << std::endl;
			// !OPTIONS1
			mEnvSS << "'QVMT'" << std::endl;
			// !NMESH SIGMAS Z(NSSP)
			mEnvSS << 0 << FILE_SPLIT_CHAR << "0.0" << FILE_SPLIT_CHAR << mSourceBathy << std::endl;

			for (const auto& loop_pair_depth_ssp : mSourceSSPData)
			{
				mEnvSS << (int)loop_pair_depth_ssp.first << FILE_SPLIT_CHAR
					<< loop_pair_depth_ssp.second << FILE_SPLIT_CHAR
					<< FILE_END_CHAR << std::endl;
			}

			// !BOTTOM TYPE
			mEnvSS << "'A*'" << FILE_SPLIT_CHAR << "0.0" << std::endl;
			// !GRAVEL BOTTOM TYPE
			if (false) {
				mEnvSS << mSourceBathy << FILE_SPLIT_CHAR
					<< 1800 << FILE_SPLIT_CHAR
					<< 2043.72 << FILE_SPLIT_CHAR
					<< 2 << FILE_SPLIT_CHAR
					<< 0.6 << FILE_SPLIT_CHAR
					<< 1.5 << FILE_SPLIT_CHAR
					<< FILE_END_CHAR << std::endl;

			}
			else {

				auto srcSediment = std::make_shared<ax::bhp::SedimentMud>(); 
									// mDataset->getSediment(mSourceLon, mSourceLat, mSourceDepth);
				if (!srcSediment) {
					mLastErrorMessage = "AxError: bhp's sediment error";
					return false;
				}
				
				mEnvSS << mSourceBathy << FILE_SPLIT_CHAR
					<< srcSediment->getStringValues() << FILE_SPLIT_CHAR
					<< FILE_END_CHAR << std::endl;
			}

			// !SOURCE'S DEPTH
			mEnvSS << 1 << std::endl
				<< mSourceDepth << FILE_SPLIT_CHAR << FILE_END_CHAR << std::endl;

			// !RX DEPTH(S)
			mEnvSS << mDepthNumber << std::endl
				<< 0 << FILE_SPLIT_CHAR << mProfileMaxBathy << FILE_SPLIT_CHAR << FILE_END_CHAR << std::endl;

			// !RX RANGE(S)
			mEnvSS << mRangeNumber << std::endl
				<< 0 << FILE_SPLIT_CHAR << mDistance << FILE_SPLIT_CHAR << FILE_END_CHAR << std::endl;

			// !RAY OPTIONS
			mEnvSS << "'" << mRayOption_1 << mRayOption_2 << "'" << std::endl
				<< mRayNumber << std::endl;

			// !START, END ANGLES
			mEnvSS << mAngleStart << FILE_SPLIT_CHAR
				<< mAngleEnd << FILE_SPLIT_CHAR << FILE_END_CHAR << std::endl;

			// !RAY-STEP, BOX DEPTH, BOX RANGE
			mEnvSS << "'0.0'" << FILE_SPLIT_CHAR
				<< mBoxMaxDepth << FILE_SPLIT_CHAR
				<< mBoxMaxRange << FILE_SPLIT_CHAR
				<< ",";

			Poco::Path env_file_path(mCreatorPath, BELLHOP_ENVFILE_PREFIX + std::string(".env"));
			Poco::FileStream env_pos(env_file_path.toString(), std::ios::out | std::ios::trunc | std::ios::binary);
			if (!env_pos.good()) {
				throw Poco::OpenFileException("AxError: open Env File Fail!");
			}

			env_pos << mEnvSS.str();
			env_pos.close();
			if (!env_pos.good()) {
				throw Poco::WriteFileException("AxError: write Env File Fail!");
			}

			return true;
		}
		catch (...)
		{
			mLastErrorMessage = "AxError: write_env func catch a exception";
			return false;
		}
	}
	bool write_ssp()
	{
		try
		{
			mSspSS.clear();
			mSspSS << mSSPVector.size() << std::endl;

			std::vector<std::vector<double>> disSSPVector;
			for (const auto& loop_pair : mSSPVector) {
				mSspSS << std::left << std::setw(20) << loop_pair.first << FILE_SPLIT_CHAR;
				disSSPVector.push_back(std::move(loop_pair.second));
			} mSspSS << std::endl;

			int h = disSSPVector.front().size();
			int w = disSSPVector.size();

			for (int di = 0; di < h; ++di) {
				for (int i = 0; i < w; ++i) {
					mSspSS << std::left << std::setw(20) << disSSPVector[i][di] << FILE_SPLIT_CHAR;
				} mSspSS << std::endl;
			}

			Poco::Path ssp_file_path(mCreatorPath, BELLHOP_ENVFILE_PREFIX + std::string(".ssp"));
			Poco::FileStream ssp_pos(ssp_file_path.toString(), std::ios::out | std::ios::trunc | std::ios::binary);
			if (!ssp_pos.good()) {
				throw Poco::OpenFileException("AxError: open SSP File Fail!");
			}

			ssp_pos << mSspSS.str();
			ssp_pos.close();
			if (!ssp_pos.good()) {
				throw Poco::WriteFileException("AxError: write SSP File Fail!");
			}

			return true;
		}
		catch (...)
		{
			mLastErrorMessage = "AxError: write_ssp func catch a exception";
			return false;
		}
	}
	bool write_bty()
	{
		try
		{
			mBtySS.clear();
			mBtySS << "'L'" << std::endl;
			mBtySS << mBathyVector.size() << std::endl;

			for (const auto& loop_pair : mBathyVector) {
				mBtySS << loop_pair.first << FILE_SPLIT_CHAR << loop_pair.second << std::endl;
			}

			Poco::Path bty_file_path(mCreatorPath, BELLHOP_ENVFILE_PREFIX + std::string(".bty"));
			Poco::FileStream bty_pos(bty_file_path.toString(), std::ios::out | std::ios::trunc | std::ios::binary);
			if (!bty_pos.good()) {
				throw Poco::OpenFileException("AxError: open bty File Fail!");
			}

			bty_pos << mBtySS.str();
			bty_pos.close();
			if (!bty_pos.good()) {
				throw Poco::WriteFileException("AxError: write bty File Fail!");
			}

			return true;
		}
		catch (...)
		{
			mLastErrorMessage = "AxError: write_bty func catch a exception";
			return false;
		}
	}
};

} // namespace bhp
} // namespace ax

ax::bhp::BhpCaller::BhpCaller()
{
	d = std::make_shared<ax::bhp::BhpCallerPrivate>();
}

ax::bhp::BhpCaller::~BhpCaller()
{
}

void ax::bhp::BhpCaller::setBhpExePath(const std::string& bhpExePath)
{
	d->mBhpExePath = bhpExePath;
}

void ax::bhp::BhpCaller::setOceanData(const ax::ocean::Dataset::sptr& oceanData)
{
	if (oceanData) {
		d->mOceanData = oceanData;
	}
}

void ax::bhp::BhpCaller::setParam(const ax::param::Line::sptr& param)
{
	if (param) {
		d->mParam = param;
	}
}

void ax::bhp::BhpCaller::setSource(const ax::core::Source::sptr& source)
{
	if (source) {
		d->mSource = source;
	}
}

ax::res::Line::sptr ax::bhp::BhpCaller::Call()
{
	ax::res::Line::sptr result;

	do
	{
		bool exec_res = true;
		exec_res = d->parse_param();
		if (!exec_res) { return result; }
		exec_res = d->init_member();
		if (!exec_res) { return result; }
		exec_res = d->find_path();
		if (!exec_res) { return result; }
		exec_res = d->write_env();
		if (!exec_res) { return result; }
		exec_res = d->write_ssp();
		if (!exec_res) { return result; }
		exec_res = d->write_bty();
		if (!exec_res) { return result; }

	} while (false);

	static std::vector<std::string> args{ BELLHOP_ENVFILE_PREFIX };
	try
	{
		auto run_bhp_path_exe = d->mBhpExePath.empty() ? BELLHOP_EXECUTABLE : d->mBhpExePath;
		Poco::ProcessHandle ph = Poco::Process::launch(
			run_bhp_path_exe,
			args,
			d->mCreatorPath.toString()
		);

		int rc = ph.wait();
		if (rc == 0) 
		{
			ax::bhp::ShdReader shdReader;
			Poco::Path shd_file_path(d->mCreatorPath, BELLHOP_ENVFILE_PREFIX + std::string(".shd"));
			if (shdReader.readShdFile(shd_file_path.toString()))
			{
				auto shd_data = shdReader.getShdData();

				std::vector<short> bathy_vector;
				for (const auto& loop_pair : d->mBathyVector) {
					bathy_vector.push_back(loop_pair.second);
				}

				result = std::make_shared<ax::res::Line>();
				result->setBathyData(bathy_vector);
				result->setMinResValue(shd_data.min_tl);
				result->setMaxResValue(shd_data.max_tl);

				{
					std::vector<double> r_depths;
					for (const auto& loop_depth : shd_data.rx_depths) {
						r_depths.push_back(loop_depth);
					}
					std::vector<double> r_ranges;
					for (const auto& loop_range : shd_data.rx_ranges) {
						r_ranges.push_back(loop_range / 1000);
					}

					std::vector<std::string> dims;
					dims.push_back(DIM_DEPTH_NAME);
					dims.push_back(DIM_RANGE_NAME);

					std::vector<std::vector<double>> scales;
					scales.push_back(r_depths);
					scales.push_back(r_ranges);

					auto res_data = std::make_shared<darr::DimsArray<double>>(dims, scales);
					res_data->setDataNested(shd_data.tl_values);

					result->setResData(res_data);
				}

				Poco::File shd_file(shd_file_path);
				shd_file.remove();

				d->mParam->setProperty(BELLHOP_EXECUTABLE_SUCCESS_KEY, true);
				d->write_cfg();

				return result;
			}
			else
			{
				d->mLastErrorMessage = "AxError: shd parse error: " + shdReader.getLastErrorMessage();
				return result;
			}
		}
		else
		{
			d->mLastErrorMessage = "AxError: bellhop exec return code is " + std::to_string(rc);
			return result;
		}
	}
	catch (...)
	{
		d->mLastErrorMessage = "AxError: bhp exec & shd parse error";
		return result;
	}
}

std::string ax::bhp::BhpCaller::getLastErrorMessage() const
{
	return d->mLastErrorMessage;
}
